package mongotesting

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strings"
	"testing"

	"github.com/docker/cli/cli/connhelper"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/filters"
	"github.com/docker/docker/client"
	"github.com/docker/go-connections/nat"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"

	"testing-with-docker/configs"
)

const (
	image         = "mongo:5.0"
	containerPort = "27017/tcp"
)

var mongoURI string

// DockerClientFromSSH Obtain the Docker client through SSH.
func DockerClientFromSSH(ctx context.Context, daemonURL string) (*client.Client, error) {
	helper, err := connhelper.GetConnectionHelper(daemonURL)
	if err != nil {
		return nil, err
	}
	httpClient := &http.Client{
		Transport: &http.Transport{
			DialContext: helper.Dialer,
		},
	}
	cli, err := client.NewClientWithOpts(client.FromEnv,
		client.WithHTTPClient(httpClient),
		client.WithHost(helper.Host),
		client.WithDialContext(helper.Dialer),
	)
	if err != nil {
		return nil, err
	}
	cli.NegotiateAPIVersion(ctx)
	return cli, nil
}

// RunWithMongoInDocker runs the tests with
// a mongodb instance in a docker container.
func RunWithMongoInDocker(m *testing.M) int {
	var err error
	var cli *client.Client
	var hostIP, bindingHostIP string

	ctx := context.Background()

	switch urlSli := strings.SplitN(configs.DaemonURL, "://", 2); urlSli[0] {
	case "ssh":
		u, err := url.Parse(configs.DaemonURL)
		if err != nil {
			panic(err)
		}
		hostIP = u.Hostname()
		bindingHostIP = "0.0.0.0"
		cli, err = DockerClientFromSSH(context.Background(), configs.DaemonURL)
		if err != nil {
			panic(err)
		}
	default:
		hostIP = "127.0.0.1"
		bindingHostIP = hostIP

		cli, err = client.NewClientWithOpts(client.FromEnv)
		if err != nil {
			panic(err)
		}
	}

	filter := filters.NewArgs(filters.Arg("reference", image))
	images, err := cli.ImageList(context.Background(), types.ImageListOptions{Filters: filter})
	if err != nil {
		panic(err)
	}

	if len(images) == 0 {
		out, err := cli.ImagePull(context.Background(), image, types.ImagePullOptions{})
		if err != nil {
			panic(err)
		}
		defer func(out io.ReadCloser) {
			_ = out.Close()
		}(out)
		_, _ = io.Copy(os.Stdout, out)
	}

	resp, err := cli.ContainerCreate(ctx, &container.Config{
		Image: image,
		ExposedPorts: nat.PortSet{
			containerPort: {},
		},
	}, &container.HostConfig{
		PortBindings: nat.PortMap{
			containerPort: []nat.PortBinding{
				{
					HostIP:   bindingHostIP,
					HostPort: "0",
				},
			},
		},
	}, nil, nil, "")
	if err != nil {
		panic(err)
	}
	containerId := resp.ID

	// ContainerStart 根据containerID,向 docker 守护进程发送请求以启动容器。
	err = cli.ContainerStart(ctx, containerId, types.ContainerStartOptions{})
	if err != nil {
		panic(err)
	}

	// ContainerInspect 返回容器信息。
	insRes, err := cli.ContainerInspect(ctx, resp.ID)
	if err != nil {
		panic(err)
	}

	defer func() {
		// ContainerRemove 根据containerID,杀死并从 docker 主机中删除一个容器。
		err = cli.ContainerRemove(ctx, containerId, types.ContainerRemoveOptions{
			Force: true,
		})
		if err != nil {
			panic(err)
		}

		// 循环删除容器里所有的挂载
		for _, res := range insRes.Mounts {
			err := cli.VolumeRemove(ctx, res.Name, true)
			if err != nil {
				panic(err)
			}
		}
	}()

	hostPort := insRes.NetworkSettings.Ports[containerPort][0]
	mongoURI = fmt.Sprintf("mongodb://%s:%s", hostIP, hostPort.HostPort)

	return m.Run()
}

// NewClient creates a client connected to the mongo instance in docker.
func NewClient(ctx context.Context) (*mongo.Client, error) {
	// 这里再做一个保护
	if mongoURI == "" {
		return nil, fmt.Errorf("mong uri not set. Please run RunWithMongoInDocker in TestMain")
	}
	return mongo.Connect(ctx, options.Client().ApplyURI(mongoURI))
}

// NewDefaultClient creates a client connected to DefaultMongoDNS
func NewDefaultClient(c context.Context) (*mongo.Client, error) {
	return mongo.Connect(c, options.Client().ApplyURI(configs.DefaultMongoURI))
}
